/*
 * By: Werner Schuster (murphee)
 * 
 * Copyright (c) 2005-2006 Werner Schuster (murphee)
 * 
 * This file is part of the EclipseShell project. EclipseShell is subject 
 * to the Eclipse Public License (EPL) available at
 * http://www.eclipse.org/org/documents/epl-v10.php
 * or in the file /epl-v10.html in the source for other info
 * see the /eclipseshell.license file
 */
package net.sf.eclipseshell.extensions;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.URL;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;

import net.sf.eclipseshell.EclipseShellPlugin;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IRuntimeClasspathEntry;
import org.eclipse.jdt.launching.JavaRuntime;

/**
 * Sets up jars and resources for launching a remote controlled Interpreter
 * with a JVM Launch and gives access to a IRemoteHandle object to controll the remote
 * interpreter.
 * This sets up a temporary filesystem location in the workspace .metadata directory, 
 * where libraries (Jar files) and other filesystem resources for this launch will be copied.
 * This is needed for launching the remote Java scripting language interpreters, the libraries
 * to communicate with the remote JVM and startup script files.
 * 
 * @author murphee
 *
 */
public class RemoteInterpreterLaunch {

	private String launchConfigName_;
	private int port_;
	private IProject project_;
	private boolean useProjectClassPath_;
	private List bootPathPrepend_;
	File libraryPath_;
	
	
	/**
	 * 
	 * 
	 * @param configName name of the JVM LaunchConfiguration to use or create for the launch
	 * @param project the @see IProject object that this launch is supposed to use as context (for 
	 * getting the project Classpath, etc), CAN be null
	 */
	public RemoteInterpreterLaunch(String configName, IProject project) {
		launchConfigName_ = configName;
		project_ = project;
		// .metadata location to put the Jars and resources
		IPath utilLibraryPath =  Platform.getPluginStateLocation(EclipseShellPlugin.getDefault());
		// TODO: create a LaunchConfiguration dependent directory !
		
		libraryPath_ = new File(utilLibraryPath.toOSString());
		

		// TODO: cache this: if the cajo.jar and beanshell.jar and the .class file aren't there, 
		//  - get them from the file
		//  - drop them in the temp directory
		bootPathPrepend_ = new ArrayList();
		
	}
	
	/**
	 * If an IProject object has been set, and it can be mapped to a IJavaProject, then it's
	 * possible to use it's Classpath settings for the launch. If this is set to true, then 
	 * the projects Classpath is used, otherwise the classpath will only consist of the Jar files
	 * set with the addJarFile methods.
	 * 
	 * @param use whether the projects Classpath should be used
	 */
	public void setUseProjectClasspath(boolean use) {
		useProjectClassPath_ = use;
	}
	
	/**
	 * 
	 * @param port the port number to use for the communication with the remote interpreter,
	 * value 0 (zero) means that a free port will be searched
	 */
	public void setPort(int port) {
		port_ = port;
	}

	/**
	 * Adds a file resource to the temporary launch location in the filesystem and returns a 
	 * filesystem URL to access it. 
	 * 
	 * @param url a URL to load the Resource, use @see java.lang.Class getResource method to get URLs
	 * for resources in JARs
	 * @param name the filename (relative to the temporay launch location) of the resource 
	 * @return a URL for accessing the resource in the filesystem OR null if a problem occured or the reource
	 * could not be written.
	 */
	public URL addResourceUrl(URL url, String name){
		IPath jarLocation = storeResourceToStateLocation(libraryPath_, name, url);
		try {
			return jarLocation.toFile().toURL();
		} catch (MalformedURLException e) {
			EclipseShellPlugin.getDefault().log(Status.WARNING, e.getLocalizedMessage(), e);
			return null;
		}
	}
	
	/**
	 * Adds a Jar file resource to the temporary launch location in the filesystem AND adds it to 
	 * the Classpath of the launch. Returns a filesystem URL to access it. 
	 * 
	 * @param url a URL to load the Jar file as Resource, use @see java.lang.Class getResource method to get URLs
	 * for resources in JARs
	 * @param name the filename (relative to the temporay launch location) of the resource
	 * @return a URL for accessing the Jar file in the filesystem OR null if a problem occured or the reource
	 * could not be written.
	 */
	public URL addJarFile(URL url, String name){
		String tempClasspathMemento = null; 
		IPath jarLocation = storeResourceToStateLocation(libraryPath_, name, url);
		try {
			tempClasspathMemento  = JavaRuntime.newArchiveRuntimeClasspathEntry(jarLocation).getMemento();
			bootPathPrepend_.add(tempClasspathMemento );
		} catch (CoreException e) {
			EclipseShellPlugin.getDefault().log(Status.WARNING, e.getLocalizedMessage(), e);
		}
		try {
			return jarLocation.toFile().toURL();
		} catch (MalformedURLException e) {
			EclipseShellPlugin.getDefault().log(Status.WARNING, e.getLocalizedMessage(), e);
			return null;
		}
	}
	
	// TODO: add a way to add classpath entries from the project!
	
	public IRemoteHandle launch(String mainClass, String argString){

		ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
		ILaunchConfigurationType type = manager.getLaunchConfigurationType(IJavaLaunchConfigurationConstants.ID_JAVA_APPLICATION);
		ILaunchConfiguration[] configurations;

		port_ = getOrFindPort();	
		
			
			ILaunchConfigurationWorkingCopy workingCopy;
			try {
				configurations = manager.getLaunchConfigurations(type);
				for (int i = 0; i < configurations.length; i++) {
					ILaunchConfiguration configuration = configurations[i];
					if (configuration.getName().equals(launchConfigName_)) {
						// TODO: DON'T delete it! Instead re-use it to get settings!
						configuration.delete();
						break;
					}
				}
//				manager.
				
				
				URL cajoUrl = Platform.find(EclipseShellPlugin.getDefault().getBundle(), new Path("cajo.jar"));
				addJarFile(cajoUrl, "cajo.jar");
		
				
				
				workingCopy = type.newInstance(null, launchConfigName_);
//				workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,"bsh.Interpreter");
				workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,mainClass);
				
				// TODO: rename from beanshell.port TO something else
				workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, "-Dbeanshell.port=" + port_);

				
				workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, argString);
//				-Dbeanshell.port=8888 
				if(project_ != null){
//					String[] prePend = (String[])bootPathPrepend.toArray(new String[bootPathPrepend.size()]);
					
					// TODO: if classPathUse is true -> add the projects Classpath here!
//					if(project_.getAdapter(J))
//					if(JavaCore.getJavaCore().){
//					}
					IJavaProject javaProj = JavaCore.create(project_);
					if(javaProj != null){
						IClasspathEntry[] resolvedClasspath = javaProj.getResolvedClasspath(true);
						for (int i = 0; i < resolvedClasspath.length; i++) {
							IClasspathEntry currEntry = resolvedClasspath[i];
//							currEntry.getResolvedEntry().get
//							new Archive
//							currEntry.getResolvedEntry().
							IPath location = JavaCore.getResolvedClasspathEntry(currEntry).getPath();
							IRuntimeClasspathEntry tempRuntimeClasspathEntry = JavaRuntime.newArchiveRuntimeClasspathEntry(location);
							bootPathPrepend_.add(tempRuntimeClasspathEntry.getMemento());
						}
					}
					
					
					workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_CLASSPATH, bootPathPrepend_);
					workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_DEFAULT_CLASSPATH, false);
					
					workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, project_.getName());
				} else {
					// just add the necessary files and added Jars
					workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_CLASSPATH, bootPathPrepend_);
					workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_DEFAULT_CLASSPATH, false);
				}
//				workingCopy.setAttribute()
				
				ILaunchConfiguration configuration = workingCopy.doSave();
				DebugUITools.launch(configuration, ILaunchManager.DEBUG_MODE);
				
				
				return new CajoRemoteHandle("//localhost:" + port_ + "/remote", port_);
			} catch (CoreException e) {
				EclipseShellPlugin.getDefault().log(Status.ERROR, e.getLocalizedMessage(), e);
				return null;
			}
			
	}
	private int getOrFindPort() {
		if(port_ == 0){
			ServerSocket localsocket= null;
			try {
				localsocket= new ServerSocket(0);
				return localsocket.getLocalPort();
			} catch (IOException e) {
				EclipseShellPlugin.getDefault().log(IStatus.ERROR, "Problem while searching port for RemoteInterpreter", e);
			} finally {
				if (localsocket != null) {
					try {
						localsocket.close();
					} catch (IOException e) {
						EclipseShellPlugin.getDefault().log(IStatus.ERROR, "Exception while closing ServerSocket.", e);
					}
				}
			}
		} 
		// TODO: report this or handle 0 in some special way?
		return port_;
	}
	//	return gnu.cajo.invoke.Remote.invoke(remoteInterp_, "eval", "print("+code+")");
//	gnu.cajo.invoke.Remote.getItem("//localhost:" + port + "/remoteRuby");
	public class CajoRemoteHandle implements IRemoteHandle {
		private String rmiObjectUrl_;
		int remotePort_;
		public CajoRemoteHandle(String rmiUrlForObject, int port){
			this.remotePort_ = port;
			this.rmiObjectUrl_ = rmiUrlForObject;
		}

		public void shutdown(){
			// TODO: what todo here? 
		}
		
		public Object execute(String methodName, Object[] args) {
			try {
						
					Object remoteInterpreter = gnu.cajo.invoke.Remote.getItem(rmiObjectUrl_);
					return gnu.cajo.invoke.Remote.invoke(remoteInterpreter, methodName, args[0]);
				
			} catch (Exception e) {
				EclipseShellPlugin.getDefault().log(Status.WARNING, e.getLocalizedMessage(), e);
			} 
			return null;
		}
		
		public Object complete(String prefix){
			String completerUrl = "//localhost:"+remotePort_+"/completer";
			Object completer;
			try {
				completer = gnu.cajo.invoke.Remote.getItem(completerUrl);
				Object object = gnu.cajo.invoke.Remote.invoke(completer, "complete",null);
				return object;
			} catch (Exception e) {
				EclipseShellPlugin.getDefault().log(Status.WARNING, e.getLocalizedMessage(), e);
			}
			return null;
		
		}
		
	}
		
	private IPath storeResourceToStateLocation(File libraryPath, String filename, URL cajoUrl) {
		try {
			InputStream is = cajoUrl.openStream();
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			byte[] tempBuf = new byte[1024];
			int read = 0;
			while( (read =  is.read(tempBuf)) >= 0){
				bos.write(tempBuf, 0, read);
			}
			bos.close();
			is.close();
			Path file = new Path(new File(libraryPath, filename).getAbsolutePath());
			FileOutputStream fw = new FileOutputStream(file.toFile());
			fw.write(bos.toByteArray());
			fw.close();
			return file;
		} catch (IOException e) {
			EclipseShellPlugin.getDefault().log(Status.WARNING, e.getLocalizedMessage(), e);
		}
		return null;
	}
	
	
}